#region Copyright Notices
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Library General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Library General Public License for more details.
// 
// You should have received a copy of the GNU Library General Public
// License along with this library; if not, write to the Free
// Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#endregion

using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Globalization;

namespace Yarr
{
    /// <summary>
    /// An Yarr function (built-in or user-defined)
    /// </summary>
    /// <param name="arguments"></param>
    /// <returns></returns>
    public delegate Object Fn (params object[] arguments); 


    /// <summary>
    /// Wraps an Yarr function -- either built-in or user-defined
    /// </summary>
    internal class FunctionBinding
    {
        private static MethodInfo predicateMethod = 
            typeof(FunctionBinding).GetMethod("PredicateImpl", BindingFlags.Instance | 
                                                               BindingFlags.NonPublic);

        private static MethodInfo func0Method =
            typeof(FunctionBinding).GetMethod("FuncImpl0", BindingFlags.Instance |
                                                           BindingFlags.NonPublic);

        private static MethodInfo func1Method =
            typeof(FunctionBinding).GetMethod("FuncImpl1", BindingFlags.Instance |
                                                           BindingFlags.NonPublic);

        private static MethodInfo func2Method =
            typeof(FunctionBinding).GetMethod("FuncImpl2", BindingFlags.Instance |
                                                           BindingFlags.NonPublic);
        private static MethodInfo func3Method =
            typeof(FunctionBinding).GetMethod("FuncImpl3", BindingFlags.Instance |
                                                           BindingFlags.NonPublic);
        private static MethodInfo func4Method =
            typeof(FunctionBinding).GetMethod("FuncImpl4", BindingFlags.Instance |
                                                           BindingFlags.NonPublic);

        private static MethodInfo action0Method =
            typeof(FunctionBinding).GetMethod("ActionImpl0", BindingFlags.Instance |
                                                             BindingFlags.NonPublic);

        private static MethodInfo action1Method =
            typeof(FunctionBinding).GetMethod("ActionImpl1", BindingFlags.Instance |
                                                             BindingFlags.NonPublic);

        private static MethodInfo action2Method =
            typeof(FunctionBinding).GetMethod("ActionImpl2", BindingFlags.Instance |
                                                             BindingFlags.NonPublic);
        private static MethodInfo action3Method =
            typeof(FunctionBinding).GetMethod("ActionImpl3", BindingFlags.Instance |
                                                             BindingFlags.NonPublic);
        private static MethodInfo action4Method =
            typeof(FunctionBinding).GetMethod("ActionImpl4", BindingFlags.Instance |
                                                             BindingFlags.NonPublic);

        private Closure c;

        public Closure Closure { get { return c; } }

        private FunctionBinding (Closure c)
        {
            this.c = c;
        }

        private Object Impl (params object[] arguments)
        {
            Cons cons = null;

            if (arguments.Length > 0)
                cons = Cons.FromArray(arguments);

            return c.Invoke(cons);
        }


        private bool PredicateImpl<T>(T argument)
        {
            return Conversions.ObjectToBoolean(c.Invoke(new Cons(argument)));
        }

        private TR FuncImpl0<TR>()
        {
            return (TR)c.Invoke(null);
        }

        private TR FuncImpl1<T1, TR>(T1 arg1)
        {
            return (TR)c.Invoke(new Cons(arg1));
        }

        private TR FuncImpl2<T1, T2, TR>(T1 arg1, T2 arg2)
        {
            return (TR)c.Invoke(new Cons(arg1, new Cons(arg2)));
        }

        private TR FuncImpl3<T1, T2, T3, TR>(T1 arg1, T2 arg2, T3 arg3)
        {
            return (TR)c.Invoke(new Cons(arg1, new Cons(arg2, new Cons(arg3))));
        }

        private TR FuncImpl4<T1, T2, T3, T4, TR>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
        {
            return (TR)c.Invoke(new Cons(arg1, new Cons(arg2, new Cons(arg3, new Cons(arg4)))));
        }

        private void ActionImpl0()
        {
            c.Invoke(null);
        }

        private void ActionImpl1<T1>(T1 arg1)
        {
            c.Invoke(new Cons(arg1));
        }

        private void ActionImpl2<T1, T2>(T1 arg1, T2 arg2)
        {
            c.Invoke(new Cons(arg1, new Cons(arg2)));
        }

        private void ActionImpl3<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
        {
            c.Invoke(new Cons(arg1, new Cons(arg2, new Cons(arg3))));
        }

        private void ActionImpl4<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
        {
            c.Invoke(new Cons(arg1, new Cons(arg2, new Cons(arg3, new Cons(arg4)))));
        }

        /// <summary>
        /// 
        /// </summary>
        public Fn BoundFunction
        {
            get { return Impl; }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cl"></param>
        /// <returns></returns>
        public static Fn Bind (Closure cl)
        {
            if (cl == null)
                return null;

            return new FunctionBinding(cl).BoundFunction;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cl"></param>
        /// <returns></returns>
        public static Delegate BindPredicate(Closure cl)
        {
            if (cl.Arguments == null  ||  cl.Arguments.Length != 1)
                throw new YarrException("BindPredicate: Predicate must take exactly one argument");

            Object arg = cl.Arguments.First();

            Type predArgType = typeof(Object);

            if (arg is Cons)
                predArgType = TypeCache.FindType(((Cons)arg).Cadr().ToString());

            Type[] predTypeArray = new Type[] { predArgType };
            Type PredType = TypeCache.FindType("Predicate", predTypeArray);
            if (PredType != null)
            {
                MethodInfo predTypeMethod = predicateMethod.MakeGenericMethod(predTypeArray);

                if (predTypeMethod != null)
                {
                    FunctionBinding fb = new FunctionBinding(cl);
                    return Delegate.CreateDelegate(PredType, fb, predTypeMethod, true);
                }
            }

            throw new YarrException("BindPredicate: Unable to create a delegate for closure");
        }

        public static Delegate BindFuncDelegate(Closure cl)
        {
            return BindDelegateHelper(cl, "Func", true);
        }

        public static Delegate BindActionDelegate(Closure cl)
        {
            return BindDelegateHelper(cl, "Action", false);
        }

        public static Expression BindExpression(Closure cl)
        {
            FunctionBinding fb = new FunctionBinding(cl);

            Type[] argTypes = GetArgumentTypes(cl, "Func", true);
            Type FuncType = TypeCache.FindType("Func", argTypes);
            MethodInfo mi = GetMethodInfo(argTypes, true);

            ParameterExpression[] parameters = new ParameterExpression[argTypes.Length - 1];

            Cons args = cl.Arguments;
            for (int ix = 0; ix < argTypes.Length - 1; ++ix)
            {
                Object nextArg = args.Car();
                string paramName = (nextArg is Cons) ? ((Cons)nextArg).Car().ToString() : nextArg.ToString();
                parameters[ix] = Expression.Parameter(argTypes[ix], paramName);
                args = (Cons)args.Cdr();
            }

            Expression body = Expression.Call(Expression.Constant(fb), mi, parameters);
            return Expression.Lambda(FuncType, body, parameters);
        }

        private static Delegate BindDelegateHelper(Closure cl, string baseName, bool includeReturnType)
        {
            Type[] argTypes = GetArgumentTypes(cl, baseName, includeReturnType);

            Type FuncType = TypeCache.FindType(baseName, argTypes);

            FunctionBinding fb = new FunctionBinding(cl);

            MethodInfo mi = GetMethodInfo(argTypes, includeReturnType);

            return Delegate.CreateDelegate(FuncType, fb, mi, true);
        }

        private static Type[] GetArgumentTypes(Closure cl, string baseName, bool includeReturnType)
        {
            Type[] argTypes = cl.GetArgumentTypes(includeReturnType);

            if (argTypes == null)
                throw new YarrException(
                    String.Format(CultureInfo.CurrentCulture, 
                        "BindDelegate: {0}<...> does not support special parameter types", baseName));

            if (argTypes.Length > 4)
                throw new YarrException(
                    String.Format(CultureInfo.CurrentCulture, 
                            "BindDelegate: {0}<...> supports at most four parameters", baseName));

            return argTypes;
        }

        private static MethodInfo GetMethodInfo(Type[] argTypes, bool includeReturnType)
        {
            int argLength = argTypes.Length - (includeReturnType ? 1 : 0);

            switch (argLength)
            {
                case 0:
                    return includeReturnType ?
                            func0Method.MakeGenericMethod(argTypes) :
                            action0Method;

                case 1:
                    return includeReturnType ?
                            func1Method.MakeGenericMethod(argTypes) :
                            action1Method.MakeGenericMethod(argTypes);

                case 2:
                    return includeReturnType ?
                            func2Method.MakeGenericMethod(argTypes) :
                            action2Method.MakeGenericMethod(argTypes);

                case 3:
                    return includeReturnType ?
                            func3Method.MakeGenericMethod(argTypes) :
                            action3Method.MakeGenericMethod(argTypes);

                case 4:
                    return includeReturnType ?
                            func4Method.MakeGenericMethod(argTypes) :
                            action4Method.MakeGenericMethod(argTypes);

                default:
                    return null;
            }
        }
    } 
}
